@file:UseSerializers(ItemRequirementStorageSerializer::class)

package com.lalameow.researchstation.block

import com.lalameow.researchstation.BlockEntities
import com.lalameow.researchstation.Nbt
import com.lalameow.researchstation.ResearchStation
import com.lalameow.researchstation.api.technology.FluidRequirement
import com.lalameow.researchstation.api.technology.ItemRequirement
import com.lalameow.researchstation.api.technology.Technology
import com.lalameow.researchstation.api.technology.TechnologyRequirement.Progressing
import com.lalameow.researchstation.api.technology.progressing
import com.lalameow.researchstation.screen.ResearchStationScreenHandler
import com.lalameow.researchstation.translatable
import com.lalameow.researchstation.util.serialization.ItemRequirementStorageSerializer
import kotlinx.serialization.UseSerializers
import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant
import net.fabricmc.fabric.api.transfer.v1.storage.Storage
import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage
import net.fabricmc.fabric.api.transfer.v1.storage.base.FilteringStorage
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext
import net.minecraft.block.Block
import net.minecraft.block.BlockEntityProvider
import net.minecraft.block.BlockState
import net.minecraft.block.HorizontalFacingBlock
import net.minecraft.block.Material
import net.minecraft.block.Waterloggable
import net.minecraft.block.entity.BlockEntity
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemPlacementContext
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket
import net.minecraft.screen.NamedScreenHandlerFactory
import net.minecraft.screen.ScreenHandlerContext
import net.minecraft.state.StateManager
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.world.BlockView
import net.minecraft.world.World
import net.silkmc.silk.nbt.serialization.encodeToNbtElement
import net.silkmc.silk.nbt.set

@Suppress("OVERRIDE_DEPRECATION")
object ResearchStationBlock :
    HorizontalFacingBlock(
        FabricBlockSettings.of(Material.METAL).hardness(4f).resistance(64f).luminance(8).nonOpaque()
    ),
    BlockEntityProvider,
    Waterloggable {
    override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
        builder.add(FACING)
    }

    override fun getPlacementState(ctx: ItemPlacementContext) = defaultState.with(FACING, ctx.playerFacing.opposite)!!

    override fun createBlockEntity(pos: BlockPos, state: BlockState) =
        BlockEntities.RESEARCH_STATION.instantiate(pos, state)

    override fun getAmbientOcclusionLightLevel(state: BlockState, world: BlockView, pos: BlockPos) = 1.0f

    override fun onUse(
        state: BlockState,
        world: World,
        pos: BlockPos,
        player: PlayerEntity,
        hand: Hand,
        hit: BlockHitResult
    ): ActionResult {
        if (world.isClient) return ActionResult.SUCCESS
        world.getBlockEntity(pos).also { if (it is ResearchStationBlockEntity) player.openHandledScreen(it) }
        return ActionResult.CONSUME
    }
}

@Suppress("UnstableApiUsage")
class ResearchStationBlockEntity(pos: BlockPos, state: BlockState) :
    BlockEntity(BlockEntities.RESEARCH_STATION, pos, state), NamedScreenHandlerFactory {

    var technology: Technology? = null

    var requirements: Set<Progressing<*>>? = null
    lateinit var itemStorage: Storage<ItemVariant>
    lateinit var fluidStorage: Storage<FluidVariant>
    override fun createMenu(syncId: Int, inv: PlayerInventory, player: PlayerEntity) =
        ResearchStationScreenHandler(syncId, inv, ScreenHandlerContext.create(world, pos))

    fun lock(technology: Technology) {
        this.technology = technology
        requirements = technology.requirements.progressing()
        itemStorage =
            FilteringStorage.insertOnlyOf(
                CombinedStorage(
                    requirements!!.filterIsInstance<ItemRequirement.Progressing>()
                        .map { RequirementStorage(it, ItemVariant.blank()) }
                )
            )
        fluidStorage =
            FilteringStorage.insertOnlyOf(
                CombinedStorage(
                    requirements!!.filterIsInstance<FluidRequirement.Progressing>()
                        .map { RequirementStorage(it, FluidVariant.blank()) }
                )
            )
    }

    private fun unlock() {
        technology = null
        requirements = null
        itemStorage = CombinedStorage(listOf())
        fluidStorage = CombinedStorage(listOf())
    }

    fun complete() = if (requirements!!.all { it.complete() }) {
        unlock()
        true
    } else false

    override fun toUpdatePacket() = BlockEntityUpdateS2CPacket.create(this)!!

    override fun getDisplayName() = if (technology == null) ResearchStation.translatable("gui.researching_tree")
    else ResearchStation.translatable("gui.technology")

    override fun writeNbt(nbt: NbtCompound) {
        technology?.also { nbt["technology"] = it.id.toString() }
        requirements?.also { nbt.put("requirements", ResearchStation.Nbt.encodeToNbtElement(requirements)) }
        if (::itemStorage.isInitialized) {
            nbt["items"] = ResearchStation.Nbt.encodeToNbtElement(itemStorage)
        }
        if (::fluidStorage.isInitialized) {
            nbt["fluid"] = ResearchStation.Nbt.encodeToNbtElement(fluidStorage)
        }
    }
}

@Suppress("UnstableApiUsage")
class RequirementStorage<Variant : TransferVariant<*>, out Requirement : Progressing<Variant>>(
    val requirement: Requirement,
    private val blankVariant: Variant
) : SingleVariantStorage<Variant>() {
    override fun canInsert(variant: Variant) = requirement.resource.test(variant)

    override fun getCapacity(variant: Variant) = requirement.resource.count

    override fun getBlankVariant() = blankVariant

    override fun insert(
        insertedVariant: Variant,
        maxAmount: Long,
        transaction: TransactionContext
    ) = super.insert(insertedVariant, maxAmount, transaction).also { requirement.current = it }
}
